/*
 * Copyright 2010 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package java.io;

import elemental2.dom.DomGlobal;
import elemental2.webstorage.Storage;
import elemental2.webstorage.WebStorageWindow;
import jsinterop.annotations.JsPackage;
import jsinterop.annotations.JsType;

/**
 * Saves binary data to the local storage; currently using hex encoding. The string is prefixed with
 * "hex:"
 *
 * @author haustein
 */
public class RandomAccessFile implements DataOutput, DataInput, Closeable {

  public static final Storage LocalStorage = WebStorageWindow.of(DomGlobal.window).localStorage;
  /*
      public final FileDescriptor getFD() throws IOException { }
      public final FileChannel getChannel() { }
  */
  String name = null;
  final boolean writeable;
  boolean dirty;
  String data;
  int newDataPos;
  StringBuilder newData;
  int pos;
  int len;
  DataInputStream dis = new DataInputStream(new RafInputStream());
  DataOutputStream dos = new DataOutputStream(new RafOutputStream());

  public RandomAccessFile(String name, String mode) throws FileNotFoundException {
    this(new File(name), mode);
  }

  public RandomAccessFile(File file, String mode) throws FileNotFoundException {
    name = file.getCanonicalPath();

    mode = mode.toLowerCase();
    if (!mode.equals("r") && !mode.equals("rw")) {
      throw new IllegalArgumentException("mode: '" + mode + "'");
    }
    writeable = mode.equals("rw");
    if (file.exists()) {
      data = atob(LocalStorage.getItem(name));
      len = data.length();
    } else if (writeable) {
      data = "";
      dirty = true;
      try {
        flush();
      } catch (IOException e) {
        throw new FileNotFoundException("" + e);
      }
    } else {
      throw new FileNotFoundException(name);
    }
  }

  static String atob(String s) {
    return WindowHelper.atob(s);
  } /*-{
    	return $wnd.atob(s);
    }-*/;

  void flush() throws IOException {
    if (!dirty) {
      return;
    }
    consolidate();
    LocalStorage.setItem(name, btoa(data));
    dirty = false;
  }

  private void consolidate() {
    if (newData == null) {
      return;
    }

    //    	System.out.println("consolidate(); ndp: " + newDataPos + " nd-len: " + newData.length());

    if (data.length() < newDataPos) {
      StringBuilder filler = new StringBuilder();
      while (data.length() + filler.length() < newDataPos) {
        filler.append('\0');
      }
      //    	  System.out.println("padding " + (filler.length()) + " zeros in consolidate ");

      data += filler.toString();
    }

    int p2 = newDataPos + newData.length();
    data =
        data.substring(0, newDataPos)
            + newData.toString()
            + (p2 < data.length() ? data.substring(p2) : "");
    newData = null;
  }

  static String btoa(String s) {
    return WindowHelper.btoa(s);
  } /*-{
    	return $wnd.btoa(s);
    }-*/;

  public int read() throws IOException {
    return dis.read();
  }

  public int read(byte b[], int off, int len) throws IOException {
    return dis.read(b, off, len);
  }

  public int read(byte b[]) throws IOException {
    return dis.read(b);
  }

  public final void readFully(byte b[]) throws IOException {
    dis.readFully(b);
  }

  public final void readFully(byte b[], int off, int len) throws IOException {
    dis.readFully(b, off, len);
  };

  public int skipBytes(int n) throws IOException {
    return dis.skipBytes(n);
  }

  public void write(byte b[]) throws IOException {
    dos.write(b);
  }

  public void write(byte b[], int off, int len) throws IOException {
    dos.write(b, off, len);
  }

  public long getFilePointer() throws IOException {
    return pos;
  }

  public void seek(long pos) throws IOException {
    if (pos < 0) {
      throw new IllegalArgumentException();
    }
    this.pos = (int) pos;
  }

  public long length() throws IOException {
    return len;
  }

  public void setLength(long newLength) throws IOException {
    if (len != newLength) {
      consolidate();
      if (data.length() > newLength) {
        data = data.substring(0, (int) newLength);
        len = (int) newLength;
      } else {
        //    			System.out.println("padding " + (newLength - len) + " zeros in setLength to " +
        // newLength);
        while (len < newLength) {
          write(0);
        }
      }
    }
  }

  public void write(int b) throws IOException {
    dos.write(b);
  }

  public void close() throws IOException {
    if (data != null) {
      flush();
      data = null;
    }
  }

  public final boolean readBoolean() throws IOException {
    return dis.readBoolean();
  }

  public final byte readByte() throws IOException {
    return dis.readByte();
  }

  public final int readUnsignedByte() throws IOException {
    return dis.readUnsignedByte();
  }

  public final short readShort() throws IOException {
    return dis.readShort();
  }

  public final int readUnsignedShort() throws IOException {
    return dis.readUnsignedShort();
  }

  public final char readChar() throws IOException {
    return dis.readChar();
  }

  public final int readInt() throws IOException {
    return dis.readInt();
  }

  public final long readLong() throws IOException {
    return dis.readLong();
  }

  public final float readFloat() throws IOException {
    return dis.readFloat();
  }

  public final double readDouble() throws IOException {
    return dis.readDouble();
  }

  public final String readLine() throws IOException {
    return dis.readLine();
  }

  public final String readUTF() throws IOException {
    return dis.readUTF();
  }

  public final void writeBoolean(boolean v) throws IOException {
    dos.writeBoolean(v);
  }

  public final void writeByte(int v) throws IOException {
    dos.writeByte(v);
  }

  public final void writeShort(int v) throws IOException {
    dos.writeShort(v);
  }

  public final void writeChar(int v) throws IOException {
    dos.writeChar(v);
  }

  public final void writeInt(int v) throws IOException {
    dos.writeInt(v);
  }

  public final void writeLong(long v) throws IOException {
    dos.writeLong(v);
  }

  public final void writeFloat(float v) throws IOException {
    dos.writeFloat(v);
  }

  public final void writeDouble(double v) throws IOException {
    dos.writeDouble(v);
  }

  public final void writeBytes(String s) throws IOException {
    dos.writeBytes(s);
  }

  public final void writeChars(String s) throws IOException {
    dos.writeChars(s);
  }

  public final void writeUTF(String str) throws IOException {
    dos.writeUTF(str);
  }

  //    public final FileChannel getChannel() throws IOException { }

  class RafInputStream extends InputStream {

    @Override
    public int read() throws IOException {
      if (pos >= len) {
        return -1;
      } else {
        consolidate();
        return data.charAt(pos++);
        //				int p2 = (pos << 1);
        //				int result = Character.digit(data.charAt(p2), 16) * 16 +
        //  					Character.digit(data.charAt(p2 + 1), 16);
        //				pos++;
        //				return result;
      }
    }
  }

  class RafOutputStream extends OutputStream {

    public void write(int b) throws IOException {
      if (!writeable) {
        throw new IOException("not writeable");
      }

      if (newData == null) {
        newDataPos = pos;
        newData = new StringBuilder();
        //				System.out.println("no buf; newDataPos: " + pos);
      } else if (newDataPos + newData.length() != pos) {
        consolidate();
        newDataPos = pos;
        newData = new StringBuilder();
        //				System.out.println("pos mismatch; newDataPos: " + pos);
      }

      newData.append((char) (b & 255));
      //			newData.append("" + Character.forDigit((b >> 4) & 15, 16) +
      //					Character.forDigit(b & 15, 16));
      pos++;
      len = Math.max(pos, len);
      dirty = true;
    }
  }

  @JsType(isNative = true, name = "window", namespace = JsPackage.GLOBAL)
  public static class WindowHelper {

    public static native String atob(String stringToParse);

    public static native String btoa(String stringToParse);
  }
}
